package com.kws.tool.util;

import com.alibaba.fastjson.JSONObject;
import org.apache.commons.codec.binary.Base64;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.MessageFormat;
import java.util.*;
import java.util.stream.Collectors;


/**
 * @ClassName SignUtils
 * @Description
 * @Author sanyi
 * @Date 2022/6/7 11:05
 **/
public class SignatureUtils {

    private final static String method_post = "POST";
    private final static String method_get = "GET";
    private final static String accept = "*/*";
    private final static String contentType = "application/json; charset=UTF-8";
    private final static String signature = "Signature";

    //请求头
    private final static String X_Tsign_Open_App_Id = "X-Tsign-Open-App-Id";
    private final static String X_Tsign_Open_Auth_Mode = "X-Tsign-Open-Auth-Mode";
    private final static String X_Tsign_Open_Ca_Timestamp = "X-Tsign-Open-Ca-Timestamp";
    private final static String Accept = "Accept";
    private final static String Content_Type = "Content-Type";
    private final static String X_Tsign_Open_Ca_Signature = "X-Tsign-Open-Ca-Signature";
    private final static String Content_MD5 = "Content-MD5";
    private final static String Header_Date = "Date";


    public static void main(String[] args) {
        // 应用ID
        String appId = "7439043771";
        // 应用密钥
        String appKey = "350dfa8a6103e3f519b84c237e51e25d";
        // 接口调用域名
        String host = "https://smlopenapi.esign.cn";

        // 请求签名鉴权-POST请求
        testPost(appId, appKey, host);

        // 请求签名鉴权-GET请求
        testGet(appId, appKey, host);

    }

    /***
     * 请求签名鉴权-POST请求
     *
     * @param appId
     * @param appKey
     * @param host
     */
    public static void testPost(String appId, String appKey, String host) {
        // 个人创建账号api接口地址
        String apiUrl = "/v1/files/fileId";

        try {
            /** 1.构建POST请求Body体 */
            JSONObject body = new JSONObject();
            /*body.put("thirdPartyUserId", "229");
            body.put("name", "张三");
            body.put("idType", "CRED_PSN_CH_IDCARD");
            body.put("idNumber", "330621");
            body.put("mobile", "152****4800");
            body.put("email", "152****800@163.com");*/

            //输入请求头中的Date,格式需符合RFC822规范, 非必传参数
            String date = "";

            /** 2.构建签名字符串 */
            // MD5加密
            String md5 = doContentMD5(body.toString());
            // SHA256签名
            String reqSignature = doSignatureBase64(getSignStr(date, md5, apiUrl, method_post), appKey);
            System.out.println("sign： " + getSignStr(date, md5, apiUrl, method_post));

            /** 3.构建请求头 */
            LinkedHashMap<String, String> header = new LinkedHashMap<String, String>();
            header.put(X_Tsign_Open_App_Id, appId);
            header.put(X_Tsign_Open_Auth_Mode, signature);
            header.put(X_Tsign_Open_Ca_Timestamp, String.valueOf(System.currentTimeMillis()));
            header.put(Accept, accept);
            header.put(Content_Type, contentType);
            header.put(X_Tsign_Open_Ca_Signature, reqSignature);
            header.put(Content_MD5, md5);
            header.put(Header_Date, date);

            /** 4.发送POST请求*/
            String postUrl = host + apiUrl;
            String result = sendPOST(postUrl, body.toString(), header, "UTF-8");
            JSONObject resultObj = JSONObject.parseObject(result);
            System.out.println("请求返回信息： " + resultObj.toString());
        } catch (Exception e) {
            e.printStackTrace();
            String msg = MessageFormat.format("请求签名鉴权方式调用接口出现异常: {0}", e.getMessage());
            System.out.println(msg);
        }
    }

    /***
     * 请求签名鉴权-GET请求
     *
     * @param appId
     * @param appKey
     * @param host
     */
    public static void testGet(String appId, String appKey, String host) {
        // 模版管理API-查询PDF文件详情
        String apiUrl = "/v1/files/fileId";

        try {
            /** 1.构建get请求参数 */
            Map<String,Object> paramsMap = new HashMap<>();
            paramsMap.put("thirdPartyUserId", "229");
            paramsMap.put("name", "张三");
            paramsMap.put("idType", "CRED_PSN_CH_IDCARD");
            paramsMap.put("idNumber", "330621");
            paramsMap.put("mobile", "152****4800");
            paramsMap.put("email", "152****800@163.com");

            //输入请求头中的Date,格式需符合RFC822规范, 非必传参数
            String date = "";

            /** 2.构建签名字符串 */
            // GET请求时ContentMD5为""
            String md5 = "";
            // SHA256签名
            String reqSignature = doSignatureBase64(getSignStr(date, md5, getUrlWithPath(apiUrl, paramsMap, false), method_get), appKey);
            System.out.println("sign： " + getSignStr(date, md5, getUrlWithPath(apiUrl, paramsMap, false), method_get));

            /** 3.构建请求头 */
            LinkedHashMap<String, String> header = new LinkedHashMap<String, String>();
            header.put(X_Tsign_Open_App_Id, appId);
            header.put(X_Tsign_Open_Auth_Mode, signature);
            header.put(X_Tsign_Open_Ca_Timestamp, String.valueOf(System.currentTimeMillis()));
            header.put(Accept, accept);
            header.put(Content_Type, contentType);
            header.put(X_Tsign_Open_Ca_Signature, reqSignature);
            header.put(Content_MD5, md5);
            header.put(Header_Date, date);

            /** 4.发送GET请求*/
            String getUrl = host + getUrlWithPath(apiUrl, paramsMap, true);
            String result = sendGet(getUrl, header, "UTF-8");
            JSONObject resultObj = JSONObject.parseObject(result);
            System.out.println("请求返回信息： " + resultObj.toString());
        } catch (Exception e) {
            e.printStackTrace();
            String msg = MessageFormat.format("请求签名鉴权方式调用接口出现异常: {0}", e.getMessage());
            System.out.println(msg);
        }
    }

    /**
     * 获取待签名的url,params.key按自然序排序
     * 注意：
     * 1.待签名字符串中的url不需要转码; isUrlEncode = false
     * 2.get请求的url需要转码（服务端接受会出现中文乱码，鉴权签名会过不了） isUrlEncode = true
     * @param url
     * @param paramsMap
     * @param isUrlEncode  是否进行urlEncode转码；
     * @return
     */
    private static String getUrlWithPath(String url, Map<String, Object> paramsMap, boolean isUrlEncode){
        SortedMap<String, Object> sortedMap = new TreeMap<>(paramsMap);

        String params = "?" + sortedMap.entrySet().stream().map(e-> {
            try {
                return e.getKey()+"="+(isUrlEncode?URLEncoder.encode(e.getValue().toString(), "UTF-8"):e.getValue());
            } catch (UnsupportedEncodingException ex) {
                ex.printStackTrace();
                return e.getKey()+"="+e.getValue();
            }
        }).collect(Collectors.joining("&"));
        url += params;
        return url;
    }

    /***
     *
     * @param str 待计算的消息
     * @return MD5计算后摘要值的Base64编码(ContentMD5)
     * @throws Exception 加密过程中的异常信息
     */
    private static String doContentMD5(String str) throws Exception {

        MessageDigest md5 = MessageDigest.getInstance("MD5");
        md5.update(str.getBytes(StandardCharsets.UTF_8));
        byte[] md5Bytes = md5.digest();
        // 把MD5摘要后的二进制数组md5Bytes使用Base64进行编码（而不是对32位的16进制字符串进行编码）
        return new String(Base64.encodeBase64(md5Bytes), StandardCharsets.UTF_8);
    }

    /**
     * 构建待签名字符串 Post
     * @param contentMD5
     * @param url
     * @return
     */
    private static String getSignStr(String date, String contentMD5, String url, String method){
        String headers = "";
        String newLine = "".equals(headers)?"":"\n";

        return method + "\n" +
                accept + "\n" +
                contentMD5 + "\n" +
                contentType + "\n" +
                date + "\n" +
                headers + newLine +
                url;
    }

    /***
     * 计算请求签名值
     *
     * @param message 待计算的消息
     * @param secret 密钥
     * @return HmacSHA256计算后摘要值的Base64编码
     * @throws Exception 加密过程中的异常信息
     */
    private static String doSignatureBase64(String message, String secret) throws NoSuchAlgorithmException, InvalidKeyException {
            String algorithm = "HmacSHA256";
            Mac hmacSha256 = Mac.getInstance(algorithm);
            byte[] keyBytes = secret.getBytes(StandardCharsets.UTF_8);
            byte[] messageBytes = message.getBytes(StandardCharsets.UTF_8);
            hmacSha256.init(new SecretKeySpec(keyBytes, 0, keyBytes.length, algorithm));
            // 使用HmacSHA256对二进制数据消息Bytes计算摘要
            byte[] digestBytes = hmacSha256.doFinal(messageBytes);
            // 把摘要后的结果digestBytes使用Base64进行编码
            return new String(Base64.encodeBase64(digestBytes), StandardCharsets.UTF_8);
    }

//-----------------------------------------Http请求部分-----------------------------------------------

    /***
     * 向指定URL发送GET方法的请求
     *
     * @param apiUrl
     * @param encoding
     * @return
     * @throws Exception
     */
    public static String sendGet(String apiUrl, LinkedHashMap<String, String> headers,
                                 String encoding) throws Exception {
        // 获得响应内容
        String http_RespContent = null;
        HttpURLConnection httpURLConnection = null;
        int http_StatusCode = 0;
        String http_RespMessage = null;
        try {
            System.out.println(">>>> 实际请求Url: " + apiUrl);

            // 建立连接
            URL url = new URL(apiUrl);
            httpURLConnection = (HttpURLConnection) url.openConnection();
            // 需要输出
            httpURLConnection.setDoOutput(true);
            // 需要输入
            httpURLConnection.setDoInput(true);
            // 不允许缓存
            httpURLConnection.setUseCaches(false);
            // HTTP请求方式
            httpURLConnection.setRequestMethod("GET");
            // 设置Headers
            if (null != headers) {
                for (String key : headers.keySet()) {
                    httpURLConnection.setRequestProperty(key, headers.get(key));
                }
            }
            // 连接会话
            httpURLConnection.connect();
            // 获得响应状态(HTTP状态码)
            http_StatusCode = httpURLConnection.getResponseCode();
            // 获得响应消息(HTTP状态码描述)
            http_RespMessage = httpURLConnection.getResponseMessage();
            // 获得响应内容
            if (HttpURLConnection.HTTP_OK == http_StatusCode) {
                // 返回响应结果
                http_RespContent = getResponseContent(httpURLConnection);
            } else {
                // 返回非200状态时响应结果
                http_RespContent = getErrorResponseContent(httpURLConnection);
                String msg =
                        MessageFormat.format("请求失败: Http状态码 = {0} , {1}", http_StatusCode, http_RespMessage);
                System.out.println(msg);
            }
        } catch (UnknownHostException e) {
            String message = MessageFormat.format("网络请求时发生异常: {0}", e.getMessage());
            Exception ex = new Exception(message);
            ex.initCause(e);
            throw ex;
        } catch (MalformedURLException e) {
            String message = MessageFormat.format("格式错误的URL: {0}", e.getMessage());
            Exception ex = new Exception(message);
            ex.initCause(e);
            throw ex;
        } catch (IOException e) {
            String message = MessageFormat.format("网络请求时发生异常: {0}", e.getMessage());
            Exception ex = new Exception(message);
            ex.initCause(e);
            throw ex;
        } catch (Exception e) {
            String message = MessageFormat.format("网络请求时发生异常: {0}", e.getMessage());
            Exception ex = new Exception(message);
            ex.initCause(e);
            throw ex;
        } finally {
            if (null != httpURLConnection) {
                httpURLConnection.disconnect();
            }
        }
        return http_RespContent;
    }

    /***
     * 向指定URL发送POST方法的请求
     *
     * @param apiUrl
     * @param data
     * @param encoding
     * @return
     * @throws Exception
     */
    public static String sendPOST(String apiUrl, String data, LinkedHashMap<String, String> headers,
                                  String encoding) throws Exception {
        // 获得响应内容
        String http_RespContent = null;
        HttpURLConnection httpURLConnection = null;
        int http_StatusCode = 0;
        String http_RespMessage = null;
        try {
            // 建立连接
            URL url = new URL(apiUrl);
            httpURLConnection = (HttpURLConnection) url.openConnection();
            // 需要输出
            httpURLConnection.setDoOutput(true);
            // 需要输入
            httpURLConnection.setDoInput(true);
            // 不允许缓存
            httpURLConnection.setUseCaches(false);
            // HTTP请求方式
            httpURLConnection.setRequestMethod("POST");
            // 设置Headers
            if (null != headers) {
                for (String key : headers.keySet()) {
                    httpURLConnection.setRequestProperty(key, headers.get(key));
                }
            }
            // 连接会话
            httpURLConnection.connect();
            // 建立输入流，向指向的URL传入参数
            DataOutputStream dos = new DataOutputStream(httpURLConnection.getOutputStream());
            // 设置请求参数
            dos.write(data.getBytes(encoding));
            dos.flush();
            dos.close();
            // 获得响应状态(HTTP状态码)
            http_StatusCode = httpURLConnection.getResponseCode();
            // 获得响应消息(HTTP状态码描述)
            http_RespMessage = httpURLConnection.getResponseMessage();
            // 获得响应内容
            if (HttpURLConnection.HTTP_OK == http_StatusCode) {
                // 返回响应结果
                http_RespContent = getResponseContent(httpURLConnection);
            } else {
                // 返回非200状态时响应结果
                http_RespContent = getErrorResponseContent(httpURLConnection);
                String msg =
                        MessageFormat.format("请求失败: Http状态码 = {0} , {1}", http_StatusCode, http_RespMessage);
                System.out.println(msg);
            }
        } catch (UnknownHostException e) {
            String message = MessageFormat.format("网络请求时发生异常: {0}", e.getMessage());
            Exception ex = new Exception(message);
            ex.initCause(e);
            throw ex;
        } catch (MalformedURLException e) {
            String message = MessageFormat.format("格式错误的URL: {0}", e.getMessage());
            Exception ex = new Exception(message);
            ex.initCause(e);
            throw ex;
        } catch (IOException e) {
            String message = MessageFormat.format("网络请求时发生异常: {0}", e.getMessage());
            Exception ex = new Exception(message);
            ex.initCause(e);
            throw ex;
        } catch (Exception e) {
            String message = MessageFormat.format("网络请求时发生异常: {0}", e.getMessage());
            Exception ex = new Exception(message);
            ex.initCause(e);
            throw ex;
        } finally {
            if (null != httpURLConnection) {
                httpURLConnection.disconnect();
            }
        }
        return http_RespContent;
    }

    /***
     * 读取HttpResponse响应内容
     *
     * @param httpURLConnection
     * @return
     * @throws UnsupportedEncodingException
     * @throws IOException
     */
    private static String getResponseContent(HttpURLConnection httpURLConnection)
            throws UnsupportedEncodingException, IOException {
        StringBuffer contentBuffer = null;
        BufferedReader responseReader = null;
        try {
            contentBuffer = new StringBuffer();
            String readLine = null;
            responseReader =
                    new BufferedReader(new InputStreamReader(httpURLConnection.getInputStream(), "UTF-8"));
            while ((readLine = responseReader.readLine()) != null) {
                contentBuffer.append(readLine);
            }
        } finally {
            if (null != responseReader) {
                responseReader.close();
            }
        }
        return contentBuffer.toString();
    }

    /***
     * 读取HttpResponse响应内容
     *
     * @param httpURLConnection
     * @return
     * @throws UnsupportedEncodingException
     * @throws IOException
     */
    private static String getErrorResponseContent(HttpURLConnection httpURLConnection)
            throws UnsupportedEncodingException, IOException {
        StringBuffer contentBuffer = null;
        BufferedReader responseReader = null;
        try {
            contentBuffer = new StringBuffer();
            String readLine = null;
            responseReader =
                    new BufferedReader(new InputStreamReader(httpURLConnection.getErrorStream(), "UTF-8"));
            while ((readLine = responseReader.readLine()) != null) {
                contentBuffer.append(readLine);
            }
        } finally {
            if (null != responseReader) {
                responseReader.close();
            }
        }
        return contentBuffer.toString();
    }
}
